/*
 * RevHttpNode 0.1.1 - Reverse HTTP proxy for NodeJS.
 *
 * Copyright 2010 Ivan Zuzak
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
 
/*
 * RevHttpNode is an simplified implementation ofthe Reverse HTTP specification
 * available at http://www.reversehttp.net/reverse-http-spec.html. The intended
 * use of the proxy is for exposing programs within browsers to rest of the WWW.
 *
 * Other projects used:
 *  - http://github.com/csausdev/log4js-node for logging
 *  - http://github.com/ncr/node.ws.js for WebSockets server
 *  - http://jsbeautifier.org/ for pretty printing JSON strings      
 */

var http = require('http');
var url = require('url');
var sys = require('sys');
var log4js = require('./lib/log4js-node');
var ws = require('./lib/node-ws-server');
var events = require('events');

function generateUUID() {
    var uuid = [], nineteen = "89AB", hex = "0123456789ABCDEF";
    for (var i=0; i<36; i++) {
      uuid[i] = hex[Math.floor(Math.random() * 16)];
    }
    uuid[14] = '4';
    uuid[19] = nineteen[Math.floor(Math.random() * 4)];
    uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
    return uuid.join('');
}

function createRevHttpProxy() {
  var revHttp = new events.EventEmitter();
  
  log4js.addAppender(log4js.consoleAppender());
  revHttp.logger = log4js.getLogger('RevHttpProxy');
  revHttp.logger.setLevel('DEBUG');
  
  setLogLevel = function(logLevel) {
    revHttp.logger.setLevel(logLevel);
  };
  
  var reqNumber = 0;
  function getNextReqNum() {
    reqNumber+=1;
    return reqNumber;
  }
  
  revHttp.Resources = {};
  
  revHttp.registerResource = function(pathRegex) {
    if (typeof pathRegex === 'undefined') { // todo - also check if path regex has form of uri path
      pathRegex = '/' + generateUUID() + '.*';
    }
  
    if (typeof revHttp.Resources[pathRegex] === 'undefined') {
      revHttp.Resources[pathRegex] = generateUUID();
      return {registrationKey : revHttp.Resources[pathRegex], success : true, realPrefix : realPrefix, pathRegex : pathRegex};
    } else {
      return {success : false, pathRegex : pathRegex};
    }
  };
  
  revHttp.unregisterResource = function(pathRegex) {
    delete revHttp.Resources[pathRegex];
  }
  
  revHttp.isValidKeyForPath = function(pathRegex, registrationKey) {
    if (typeof revHttp.Resources[pathRegex] === 'undefined' || revHttp.Resources[pathRegex] !== registrationKey) {
      return false;
    } else {
      return true;
    }
  };
  
  revHttp.getResourceServerPath = function(requestUri) {
    for (var regex in revHttp.Resources) {
      if (requestUri.match(regex)) {
        return regex
      }
    }
  
    return null;
  };
  
  revHttp.Connections = {
    serverWorkerPaths : {},
    freeServers : {},
    busyServers : {},
    queuedClients : {},
    processingClients : {},
  };
  
  revHttp.addConnection = function(connection) {
    revHttp.Connections[connection.connectionId] = connection;
  };
  
  revHttp.getConnection = function(connectionId) {
    return revHttp.Connections[connectionId];
  };
  
  revHttp.removeConnection = function(connectionId) {
    delete revHttp.Connections[connectionId];
  };
  
  revHttp.addWorker = function(pathRegex, serverConnectionId) {
    if (typeof revHttp.Connections.freeServers[pathRegex] === 'undefined') {
      revHttp.Connections.freeServers[pathRegex] = [];
    }
    
    revHttp.Connections.freeServers[pathRegex].push(serverConnectionId);
  };
  
  revHttp.getWorker = function(pathRegex) {
    if (typeof revHttp.Connections.freeServers[pathRegex] !== 'undefined' && revHttp.Connections.freeServers[pathRegex].length > 0) {
      revHttp.Connections.freeServers[pathRegex].push(revHttp.Connections.freeServers[pathRegex].shift());
      return revHttp.Connections.freeServers[pathRegex][0];
    } else {
      return null;
    }
  };
  
  revHttp.getWorkers = function(pathRegex) {
    if (typeof revHttp.Connections.freeServers[pathRegex] !== 'undefined' && revHttp.Connections.freeServers[pathRegex].length > 0) {
      return revHttp.Connections.freeServers[pathRegex];
    } else {
      return [];
    }
  };
  
  revHttp.removeWorker = function(pathRegex, serverConnectionId) {
    if (typeof revHttp.Connections.freeServers[pathRegex] !== 'undefined') {
      for (var i=0; i<revHttp.Connections.freeServers[pathRegex].length; i++) {
        if (revHttp.Connections.freeServers[pathRegex][i] === serverConnectionId) {
          revHttp.Connections.freeServers[pathRegex].splice(i, 1);
          break;
        }
      }
    }
  };
  
  revHttp.getServerWorkerPaths = function(serverConnectionId) {
    if (typeof revHttp.Connections.serverWorkerPaths[serverConnectionId] !== 'undefined') {
      return revHttp.Connections.serverWorkerPaths[serverConnectionId];
    } else {
      return [];
    }
  };
  
  revHttp.addServerWorkerPath = function(serverConnectionId, pathRegex) {
    if (typeof revHttp.Connections.serverWorkerPaths[serverConnectionId] === 'undefined') {
      revHttp.Connections.serverWorkerPaths[serverConnectionId] = [];
    } 
    
    revHttp.Connections.serverWorkerPaths[serverConnectionId].push(pathRegex);
  };
  
  revHttp.removeServerWorkerPath = function(serverConnectionId, pathRegex) {
    if (typeof revHttp.Connections.serverWorkerPaths[serverConnectionId] === 'undefined') {
      for (var i=0; i<revHttp.Connections.serverWorkerPaths[serverConnectionId].length; i++) {
        if (revHttp.Connections.serverWorkerPaths[serverConnectionId][i] === pathRegex) {
          revHttp.Connections.serverWorkerPaths[serverConnectionId].splice(i, 1);
          break;
        }
      }
    }
  };
  
  revHttp.getClientsProcessedByServer = function(serverConnectionId) {
    if (typeof revHttp.Connections.busyServers[serverConnectionId] !== 'undefined') {
      return revHttp.Connections.busyServers[serverConnectionId];
    } else {
      return [];
    }
  };
  
  revHttp.removeClientProcessedByServer = function(serverConnectionId, clientConnectionId) {
    if (typeof revHttp.Connections.busyServers[serverConnectionId] !== 'undefined') {
      for (var i=0; i<revHttp.Connections.busyServers[serverConnectionId].length; i++) {
        if (revHttp.Connections.busyServers[serverConnectionId][i] === clientConnectionId) {
          revHttp.Connections.busyServers[serverConnectionId].splice(i, 1);
          break;
        }
      }
    }
  };
  
  revHttp.addClientProcessedByServer = function(serverConnectionId, clientConnectionId) {
    if (typeof revHttp.Connections.busyServers[serverConnectionId] === 'undefined') {
      revHttp.Connections.busyServers[serverConnectionId] = [];
    }
    
    revHttp.Connections.busyServers[serverConnectionId].push(clientConnectionId);
  };

  revHttp.removeClientBeingProcessed = function(clientConnectionId) {
    if (typeof revHttp.Connections.processingClients[clientConnectionId] !== 'undefined') {
      delete revHttp.Connections.processingClients[clientConnectionId];
    }
  };
 
  revHttp.getClientBeingProcessed = function(clientConnectionId) {
    if (typeof revHttp.Connections.processingClients[clientConnectionId] !== 'undefined') {
      return revHttp.Connections.processingClients[clientConnectionId];
    } else {
      return null;
    }
  };
  
  revHttp.addToProcessingQueue = function(request, pathRegex, clientConnectionId, serverConnectionId) {
    revHttp.Connections.processingClients[clientConnectionId] = { request : request, pathRegex : pathRegex, serverConnectionId : serverConnectionId};
  };  
   
  revHttp.getQueuedClientRequests = function(pathRegex) {
    if (typeof revHttp.Connections.queuedClients[pathRegex] !== 'undefined') {
      return revHttp.Connections.queuedClients[pathRegex];
    } else {
      return [];
    }
  };
  
  revHttp.removeQueuedClient = function(pathRegex, clientConnectionId) {
    if (typeof revHttp.Connections.queuedClients[pathRegex] !== 'undefined') {
      for (var i=0; i<revHttp.Connections.queuedClients[pathRegex].length; i++) {
        if (revHttp.Connections.queuedClients[pathRegex][i].connectionId === clientConnectionId) {
          revHttp.Connections.queuedClients[pathRegex].splice(i, 1);
          break;
        }
      }
    }
  };
  
  revHttp.addToRequestQueue = function(pathRegex, httpRequest, clientConnectionId) {
    if (typeof revHttp.Connections.queuedClients[pathRegex] === 'undefined') {
      revHttp.Connections.queuedClients[pathRegex] = [];
    }
    
    revHttp.Connections.queuedClients[pathRegex].push({httpRequest : httpRequest, connectionId : clientConnectionId});
  };
  
  revHttp.addListener('serverConnect', function(serverConnection) {
    revHttp.addConnection(serverConnection);
  });
  
  revHttp.addListener('serverDisconnect', function(serverConnection) {
    revHttp.removeConnection(serverConnection.connectionId);
    
    var serverWorkerPaths = revHttp.getServerWorkerPaths(serverConnection.connectionId);
    
    for (var i=0; i<serverWorkerPaths.length; i++) {
      revHttp.removeWorker(serverWorkerPaths[i].pathRegex, serverConnection.connectionId);
      revHttp.removeServerWorkerPath(serverWorkerPaths[i].pathRegex, serverConnection.connectionId);
    }
    
    var clientsBeingProcessed = revHttp.getClientsProcessedByServer(serverConnection.connectionId);
    
    for (var i=0; i<clientsBeingProcessed.length; i++) {
      var clientConnection = revHttp.getConnection(clientsBeingProcessed[i]);
      revHttp.removeClientProcessedByServer(serverConnection.connectionId, clientsBeingProcessed[i]);
      var request = revHttp.getClientBeingProcessed( clientsBeingProcessed[i]);
      revHttp.removeClientBeingProcessed( clientsBeingProcessed[i]);
      revHttp.emit('clientRequest', request.request, clientConnection);
    }    
  });
  
  
  revHttp.addListener('serverRegister', function(params, serverConnection) {
    var response = {"revhttp" : {Method : "REGISTER-RESPONSE", requestId : params.requestId}};
    
    var retVal = revHttp.registerResource(params.pathRegex);
    
    response.revhttp.pathRegex = retVal.pathRegex;
    response.revhttp.success = retVal.success;
    response.revhttp.registrationKey = retVal.registrationKey;
    response.revhttp.realPrefix = retVal.realPrefix;
    
    serverConnection.send(JSON.stringify(response));
  });
  
  // todo - switch json.stringify to base method so it doesnt repeat
  // todo - add some kind of copy-properties method that copies properties from one object to another
  
  revHttp.addListener('serverRequest', function(params, serverConnection) {
    var response = {"revhttp" : {Method : "REQUEST-RESPONSE", requestId : params.requestId}};
    
    if (typeof params.pathRegex === 'undefined') {
      var retVal = revHttp.registerResource(params.pathRegex);
      params.pathRegex = retVal.pathRegex;
      params.registrationKey = retVal.registrationKey;
    }
    
    response.pathRegex = retVal.pathRegex;
    response.registrationKey = retVal.registrationKey;
    
    if (revHttp.isValidKeyForPath(params.pathRegex, params.registrationKey)) {
      revHttp.addWorker(params.pathRegex, serverConnection.connectionId);
      response.revhttp.success = true;
    } else {
      response.revhttp.success = false;
    }
    
    serverConnection.send(JSON.stringify(response));
  });
  
  // todo - add requestId-s to other responses
  revHttp.addListener('serverUnrequest', function(params, serverConnection) {
    if (revHttp.isValidKeyForPath(params.pathRegex, params.registrationKey)) {
      revHttp.removeWorker(params.pathRegex, serverConnection.connectionId);      
      var response = JSON.stringify({"revhttp" : {Method : "UNREQUEST-RESPONSE", pathRegex : params.pathRegex, registrationKey : params.registrationKey, success : true}});
      serverConnection.send(response);
    } else {
      var response = JSON.stringify({"revhttp" : {Method : "UNREQUEST-RESPONSE", pathRegex : params.pathRegex, registrationKey : params.registrationKey, success : false}});
      serverConnection.send(response);
    }
  });
  
  revHttp.addListener('serverUnregister', function(params, serverConnection) {
    if (revHttp.isValidKeyForPath(params.pathRegex, params.registrationKey)) {
      revHttp.unregisterResource(params.pathRegex);
      
      var registrationWorkers = revHttp.getWorkers(params.pathRegex);
      
      for (var i=0; i<registrationWorkers.length; i++) {
        var workerConnection = revHttp.getConnection(registrationWorkers[i]);
        revHttp.removeServerWorkerPath(registrationWorkers[i], params.pathRegex);
        var response = JSON.stringify({"revhttp" : {Method : "UNREGISTER-RESPONSE", pathRegex : params.pathRegex, registrationKey : params.registrationKey, success : true}});
        workerConnection.send(response);
      }
      
      var queuedclients = revHttp.getQueuedClientRequests(params.pathRegex);
    
      for (var i=0; i<queuedclients.length; i++) {
        var clientConnection = revHttp.getConnection(queuedclients[i].connectionId);
        revHttp.removeQueuedClient(params.pathRegex, queuedclients[i].connectionId);
        revHttp.emit('clientRequest', queuedclients[i].request, clientConnection);
      }  
      
      var response = JSON.stringify({"revhttp" : {Method : "UNREGISTER-RESPONSE", pathRegex : params.pathRegex, registrationKey : params.registrationKey, success : true}});
      serverConnection.send(response);
    } else {
      var response = JSON.stringify({"revhttp" : {Method : "UNREGISTER-RESPONSE", pathRegex : params.pathRegex, registrationKey : params.registrationKey, success : false}});
      serverConnection.send(response);
    }
  });
  
  revHttp.addListener('serverRequestResponse', function(params, serverConnection) {
    var clientConnection = revHttp.getConnection(params.revhttp.requestId);
    revHttp.removeClientBeingProcessed(params.revhttp.requestId);
    revHttp.removeClientProcessedByServer(serverConnection.connectionId, params.revhttp.requestId);
    clientConnection.send(params.http);
  });
  
  revHttp.addListener('clientConnect', function(clientConnection) {
    revHttp.addConnection(clientConnection);
  });
  
  revHttp.addListener('clientDisconnect', function(clientConnection) {
    revHttp.removeConnection(clientConnection.connectionId);
    var request = revHttp.getClientBeingProcessed(clientConnection.connectionId);
    if (request !== null) {
      revHttp.removeClientProcessedByServer(request.serverConnectionId, clientConnection.connectionId);
      revHttp.removeQueuedClient(request.pathRegex, clientConnection.connectionId);
    }
    revHttp.removeClientBeingProcessed(clientConnection.connectionId);
  });

  revHttp.addListener('clientRequest', function(httpRequest, clientConnection) {
    var pathRegex = revHttp.getResourceServerPath(httpRequest.url)
    if (pathRegex !== null) {
      var serverConnectionId = revHttp.getWorker(pathRegex);
      if (serverConnectionId !== null) {
        revHttp.addToProcessingQueue(httpRequest, pathRegex, clientConnection.connectionId, serverConnectionId);
        revHttp.addClientProcessedByServer(serverConnectionId, clientConnection.connectionId);
        var response = JSON.stringify({"revhttp" : {Method : "REQUEST", pathRegex : pathRegex, requestId : clientConnection.connectionId}, "http" : httpRequest });
        var serverConnection = revHttp.getConnection(serverConnectionId);
        serverConnection.send(response);
      } else {
        revHttp.addToRequestQueue(pathRegex, httpRequest, clientConnection);
      }
    } else {
      clientConnection.send( {statusCode : 404, reasonPhrase : "Not Found", headers : {}} );
    }
  });
  
  listen = function(clientsPort, clientsHost, serversPort, serverHost) {  
    
    function setupConnection(connection) {
      connection.connectionId = generateUUID();
      
      var loggerPrefix = connection.side + "-" + getNextReqNum().toString();
      connection.logger = log4js.getLogger(loggerPrefix);
      connection.logger.setLevel('DEBUG');
      
      if (connection.side === "client") {
        connection.request.connection.setTimeout(0);
        connection.send = function (message) {
          connection.response.writeHead(message.statusCode, message.reasonPhrase, message.headers);
          if (typeof message.body !== "undefined" && message.body !== null) {
            connection.response.write(message.body);
          }
          connection.response.close();
        };
        
        connection.request.connection.addListener('end', function() {
          connection.logger.debug('Client closed.');
          revHttp.emit('clientDisconnect', connection);
        });
      } else if (connection.side === "server") {
        connection.send =  connection.websocket.write;
      }
    }
  
    revHttp.clientsServer = http.createServer(function(request, response) {
      var connection = { side : "client", request : request, response : response };
      setupConnection(connection);
      
      revHttp.emit('clientConnect', connection);
      connection.logger.debug('New client connected.');
      
      var httpRequest = {method : request.method, url : request.url, httpVersion : "HTTP/" + request.httpVersion, headers : request.headers};
      
      request.addListener("data", function (chunk) { 
        httpRequest.body = typeof httpRequest.body === "undefined" ? chunk : httpRequest.body + chunk;
      });
      
      request.addListener("end", function() {
        connection.logger.debug('Received client request.');
        revHttp.emit('clientRequest', httpRequest, connection);
      });
    });
    
    revHttp.serversServer = ws.createServer(function(websocket) {
      var connection = { connectionType : "ws", side : "server", websocket : websocket };
      setupConnection(connection);
        
      websocket.addListener("connect", function (resource) {
        revHttp.emit('serverConnect', connection);
        connection.logger.debug("New server connected.");
      });
      
      websocket.addListener("data", function (data) {
        var message = JSON.parse(data);
        
        if (typeof message.revhttp === "undefined" || message.revhttp === null) {
          connection.logger.debug("No revhttp header found, closing connection.");
          connection.close();
        } else if (typeof message.revhttp.Method === 'undefined') {
          connection.logger.debug("No revhttp Method found, closing connection.");
          connection.close();
        } else if (typeof message.revhttp.requestId === 'undefined') {
          connection.logger.debug("No revhttp requestId found, closing connection.");
          connection.close();
        } else {    
          switch (message.revhttp.Method) {
            case "REGISTER":
              revHttp.emit('serverRegister', message.revhttp, connection);
              connection.logger.debug("Received Register request.");
              break;
            case "UNREGISTER":
              revHttp.emit('serverUnregister', message.revhttp, connection);
              connection.logger.debug("Received Unregister request.");
              break;
            case "REQUEST":
              revHttp.emit('serverRequest', message.revhttp, connection);
              connection.logger.debug("Received Request request.");
              break;
            case "UNREQUEST":
              revHttp.emit('serverUnrequest', message.revhttp, connection);
              connection.logger.debug("Received Unrequest request.");
              break;
            case "REQUEST-RESPONSE":
              revHttp.emit('serverRequestResponse', message, connection);
              connection.logger.debug("Received Request response.");
              break;
            default:
              connection.logger.debug("Unknown revhttp method " + message.revhttp.Method + "found, closing server connection.");
              connection.close();
          }  
        } 
      });

      connection.websocket.addListener("close", function () {
        revHttp.emit('serverDisconnect', connection);
        connection.logger.debug("Server closed.");
      });
    });
    
    revHttp.clientsServer.listen(clientsPort, clientsHost);
    revHttp.serversServer.listen(serversPort, serversHost);

    revHttp.logger.warn('Proxy started: client-side on ' + clientsHost + ":" + clientsPort.toString() + ' server-side on ' + serversHost + ":" + serversPort.toString());
  };
  
  close = function() {
    proxyLogger.warn('Proxy stopped.');
    revHttp.clientsServer.close();
    revHttp.serversServer.close();
  };
  
  return {
    listen : listen,
    close : close,
    setLogLevel : setLogLevel
  };
}

var realPrefix = 'http://tirion.zemris.fer.hr:9899';

var serversHost = '192.168.8.129', serversPort = 9898,
    clientsHost = '192.168.8.129', clientsPort = 9899;

createRevHttpProxy().listen(clientsPort, clientsHost, serversPort, serversHost);